package edu.ucla.multicastim;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

import edu.ucla.multicastim.audio.MultiAudioChat;
import edu.ucla.multicastim.connection.RendezvousConn;
import edu.ucla.multicastim.rtt.SimpleRTTClient;
import edu.ucla.multicastim.rtt.SimpleRTTServer;
import edu.ucla.multicastim.util.NetworkAddressUtil;
import edu.ucla.multicastim.util.PacketParserUtil;

public class Bootstrap {
	// Contains list of children once connected to tree
	private static Vector<String> childNodeList;
	// Contains address of root Node of tree
	private static String rootNode;
	// Contains address of parent node in tree
	private static String parentNode;
	// Contains address of potential parent node in tree
	private static String potentialParent;
	// Contains address of nodes on the path from root to this client
	private static String[] rootPath;
	//private Stack<String> rootPath;
	// Contains this client's Address 
	protected static String myAddress;
	// Current minimum RTT time encountered in join phase
	private static double minRTT;
	// RTT client which Chris made keeping it separated
	private static SimpleRTTClient rttClient;
	private static SimpleRTTServer rttServer;
	
	private static Timer refreshTimer;
	private static int delay = 0;
	private static int refreshPeriod = 5000;
	private static int timerRefreshSent = 0;
	private static int randomIntRange = 4000;
	private static Timer pathImprovementTimer;
	private static int improvementPeriod = 30000;
	
	// flag variables
	private static boolean isJoining;
	private static boolean isLeaving;	
	
	
	// Stuff maybe used for packets sent
	// Please note the explicit use of spaces in some strings while not in others
	private static final String REQUESTCHILDLIST = "REQUESTCHILDREN ";
	private static final String CHILDLIST = "CHILDLIST";
	private static final String REQUESTJOIN = "JOIN ";
	private static final String ACKJOIN = "ACKJOIN ";
	private static final String NOTIFYLEAVE = "LEAVE ";
	private static final String REFRESHREQUEST = "REFRESH ";
	private static final String NOTIFYPATH = "PATH";
	private static final String DATASENT = "DATA ";
	
	private static MultiAudioChat mac = null;

	private static class LauncherThread extends Thread { 
		
		private ServerSocket srvSkt = null;
		
		public LauncherThread(ServerSocket srvSkt) { 
			super("LauncherThread");
			this.srvSkt = srvSkt;
		}
		
		@Override
		public void run() {
			while(true) {
				try {
					ListenThread lt = new ListenThread(srvSkt.accept());
					lt.start();
				} catch (IOException e) { 
					e.printStackTrace();
				}
			}
		}
	}
	
	// Modified the ListenerThread class to listen for incoming UDP packets and call a specific method depending on the type of 
	// packet received. Note, this will definitely have to change if we decide to use TCP for the control stuff.
	private static class ListenThread extends Thread {
		
	    private Socket socket = null;
		private BufferedReader in = null;		
		
		public ListenThread(Socket socket) {
			super("ListenThread");
			this.socket = socket;
		}

		@Override
		public void run() {
			try {
				in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
				
				InetAddress socketAddr = socket.getInetAddress();
				String packetAddress = socketAddr.getHostAddress();
				
				String inputLine;
				int count = 0;
	
				while ((inputLine = in.readLine()) != null) {
					System.out.println("Socket Comm waiting for packets (processed " + count + " already)... listening on: " + socket.getLocalAddress().toString());
					System.out.println("Packet (" + count + ") received, time to process...");
					
					byte[] pkt = inputLine.getBytes();
					String packetType = PacketParserUtil.getPacketType(pkt);
					String packetPayload = PacketParserUtil.getPacketPayload(pkt);
					
					if(packetType.equals("JOIN-REQUEST")) {
						System.out.println("JOIN");
						joinRequestReceived(packetAddress);
					} 
					else if(packetType.equals("JOIN-REPLY")) {
						System.out.println("ACKJOIN");
						joinRequestAcknowledged(packetAddress);
					} 
					else if(packetType.equals("REPLY-CHILDREN")) {					// These two: CHILDLIST, AND REQUESTCHILDREN
						System.out.println("CHILDLIST");							// might have gotten mixed up...
						childListReceived(packetAddress, packetPayload);				
					} 
					else if (packetType.equals("REQUEST-CHILDREN")){
						System.out.println("REQUESTCHILDREN");
						childRequestReceived(packetAddress);						
					} 
					else if (packetType.equals("NOTIFY-LEAVE")){
						System.out.println("LEAVE");
						leaveRequestReceived(packetAddress);						
					}
					else if (packetType.equals("REFRESH")){
						System.out.println("REFRESH");
						refreshUpdateReceived(packetAddress);						
					}
					else if (packetType.equals("PATH-REPLY")){
						System.out.println("PATH");
						pathUpdateReceived(packetAddress, packetPayload);						
					}
					else if (packetType.equals("DATA")){
						System.out.println("DATA");
						datagramReceived(packetAddress);						
					}
					count++;
					//String outputString = "You sent: " + new String(packet.getData(), "Cp1252");
					//System.out.println(outputString);
					//InetAddress address = packet.getAddress();
		            //int port = packet.getPort();
		            //byte[] outBuf = outputString.getBytes("UTF8");
		            //packet = new DatagramPacket(outBuf, outBuf.length, address, port);
		            //socket.send(packet);
		            //System.out.println("Packet sent");
		            //count++;
				}
	            //socket.close();
	            //System.out.println("Socket closed");
			} catch (IOException e) { 
				e.printStackTrace();
			}
		}
	}
	
	
	private static class RefreshThread extends TimerTask {
		@Override
		public void run() {
			System.out.println("Sending REFRESH packet to Parent: " +parentNode);
			if(timerRefreshSent > 3) {	// We have not gotten a PATH reply, so assume parent crashed
				System.out.println("Haven't heard back from Parent, rejoining: ");
				parentNode = null;
				membershipChanged(parentNode, childNodeList);
				try {
					this.cancel();
					startJoinTreeRepair();
					
				} catch (UnknownHostException e) {
					System.err.println("Node specified for rejoining is UnknownHost.");
					e.printStackTrace();
				} catch (InterruptedException e) {
					System.err.println("Timer was Interrupted.");
					e.printStackTrace();
				}
				//TODO: need to set a timer in case we try to contact grandparent node and never get 
				// a response
				
			}
			else{
				//comm.sendDatagram(InetAddress.getByName(parentNode), "REFRESH");
				sendCommand(NetworkAddressUtil.getHostIPString(parentNode), "REFRESH", null);
				timerRefreshSent++;
			}
		}
	}
	
	private static class ImprovementThread extends TimerTask {
		@Override
		public void run() {
			System.out.println("Improvement Timer expired, attempting rejoin. ");
		
			// If you're not the root node, then try to improve 
			if(!rootNode.equals(myAddress)){
				// Stop REFRESH messages from being sent
				refreshTimer.cancel();
				// Let your parent know you're leaving the group 
				if(parentNode != null || !parentNode.equals("")){
					//comm.sendDatagram(InetAddress.getByName(parentNode), NOTIFYLEAVE);
					sendCommand(NetworkAddressUtil.getHostIPString(parentNode), "NOTIFY-LEAVE", null);
				}
				// Let your children know you're leaving your group
				if(childNodeList.size() > 0){
					for(String child : childNodeList){
						//comm.sendDatagram(InetAddress.getByName(child), NOTIFYLEAVE);
						sendCommand(NetworkAddressUtil.getHostIPString(child), "NOTIFY-LEAVE", null);
					}
				}					
				int rand = new Random(System.currentTimeMillis()).nextInt(rootPath.length);
				isJoining = true;
				System.out.println("Joining Algorithm started - Requesting child list from: " + rootPath[rand]);
				//comm.sendDatagram(InetAddress.getByName(rootPath[rand]), REQUESTCHILDLIST);
				sendCommand(NetworkAddressUtil.getHostIPString(rootPath[rand]), "REQUEST-CHILDREN", null);
				this.cancel();
			}
		}
	}
	
	private static void membershipChanged(String prnt, Vector<String> cld) {
		String[] memberArray = null; 

		int count = -1;
		if(!prnt.trim().equals("") && prnt != null) {
			memberArray = new String[cld.size() + 1];
			memberArray[0] = prnt;
			count = 1;
		} else { 
			memberArray = new String[cld.size()];
			count = 0;
		}
		for (Iterator<String> it = cld.iterator (); it.hasNext (); ) {
			memberArray[count] = it.next();
			count++;
		}
		mac.setMembershipList(memberArray);
	}

	private static boolean sendCommand(String destinationIPAdress, String commandType, String payload) {
		Socket skt = null;
		PrintWriter ot = null;
		
		try {
			skt = new Socket(destinationIPAdress, 4445);
			ot = new PrintWriter(skt.getOutputStream(), true);
			
			byte[] buf = PacketParserUtil.createPacket(commandType, payload);
			if(buf.length > 0) {
				ot.println(new String(buf));
			}
			ot.close();
		} catch (UnknownHostException e) {
			System.err.println("Don't know about host: " + destinationIPAdress);
			return false;
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}
	
	
	
	public static void deployTimer(){
		refreshTimer.scheduleAtFixedRate(new RefreshThread(),delay, refreshPeriod);
	}
	public static void startImprovementTimer(){
		pathImprovementTimer.scheduleAtFixedRate(new ImprovementThread(), delay, improvementPeriod);
	}
	
	/**
	 *  If UMRP set you as the rootNode, you don't have to query yourself to find child
	 *  and join, since you're already the root.
	 *  TODO: I guess it's all other client's responsibilities to remake the tree in case
	 *  you became root after the root left, meaning you don't know your children 
	 * 
	 * @throws UnknownHostException
	 */
	public static void startJoinTree() throws UnknownHostException{
		// Contact Root node to start recursive process of finding suitable parent node
		
		if(!rootNode.equals(myAddress)){
			isJoining = true;
			System.out.println("Joining Algorithm started - Requesting child list from: " + rootNode);
			//comm.sendDatagram(InetAddress.getByName(rootNode), REQUESTCHILDLIST);
			sendCommand(NetworkAddressUtil.getHostIPString(rootNode), "REQUEST-CHILDREN", null);
		}
		else{
			System.out.println("I'm root node, no join required.");
			rootPath = new String[1];
			rootPath[0] = myAddress;
		}
	}
	/**
	 * Delete yourself from your RootPath and pop your grandparent as new potential parent.
	 * Wait a random delay before continuing the join algorithm until a new parent is found.
	 * The random delay is used to prevent all orphans from contacting the grandparent at the 
	 * same time.
	 * 
	 * @throws UnknownHostException
	 * @throws InterruptedException 
	 */
	private static void startJoinTreeRepair() throws UnknownHostException, InterruptedException{
		isJoining = true;
		int rand = new Random(System.currentTimeMillis()).nextInt(randomIntRange);		
		Thread.sleep(rand);
		//new Timer().wait(rand);
		if(rootPath.length > 2){
			System.out.println("Joining Repair Algorithm started - Requesting child list from: " + rootPath[rootPath.length - 3]);		
			//comm.sendDatagram(InetAddress.getByName(rootPath[rootPath.length - 3]), REQUESTCHILDLIST);
			sendCommand(NetworkAddressUtil.getHostIPString(rootPath[rootPath.length - 3]), "REQUEST-CHILDREN", null);
		}
		else{
			System.out.println("Joining Repair Algorithm started - Requesting child list from: " + rootNode);
			//comm.sendDatagram(InetAddress.getByName(rootNode), REQUESTCHILDLIST);
			sendCommand(NetworkAddressUtil.getHostIPString(rootNode), "REQUEST-CHILDREN", null);
		}
	}
	
	public void leaveGroup(){
		System.out.println("Leave Group message sent. ");
		// Method to leave group
	}
	
	// The following methods are currently invoked from the listening socket thread in 
	// SocketComm. Probably a much better way to do this....
	
	private static void childListReceived(String sourceIPAddress, String data) {
		System.out.println("Received Child List from: " + sourceIPAddress);
		if(isJoining){			
			try {
				//String data = new String(pkt.getData(),0,pkt.getLength(),"Cp1252");
				
				// Modify data string so that we remove the "CHILDLIST " portion and keep
				// only the relevant data
				System.out.println("String representation of data received: " + data);
				potentialParent = sourceIPAddress;
				if(data.indexOf(" ") != -1){
					data = data.substring(data.indexOf(" "));
					// This node has to be the minRTT we've encountered, since if it were 
					// the previous parent, we wouldn't have requested a childlist from this
					// node to begin with.
					//minRTT = pingNode(pkt.getAddress().getHostAddress());				
					
					// We assume that the data was simply a list of IPv4 addresses
					// Separated by spaces.
					String[] children = data.split("[ ]+");
					String[] rttsToCheck = new String[children.length + 1];
					rttsToCheck[0] = sourceIPAddress;
					int count = 1;
					for (String child : children){
						rttsToCheck[count] = child;
						count++;
					}
					rttClient.getRTT(rttsToCheck);
					int[][] results = rttClient.getResult();
					// assume that the 0th element has RTT, but can change to hold the average
					// of all 10 elements
					minRTT = results[0][0];
					count = 1;
					int rtt;
					for (String child : children){
						rtt = results[count][0];
						if(rtt < minRTT ){
							potentialParent = children[count - 1];
						}
					}
					// If the node we queried for it's child list was the closest node
					// to begin with, we'll stop looking at children lists and attempt to 
					// select queried node as parent
					if (potentialParent == sourceIPAddress){
						isJoining = false;
						System.out.println("Closest Node found. Requesting to join tree with parent set as: " + potentialParent);
						sendCommand(NetworkAddressUtil.getHostIPString(potentialParent), "JOIN-REQUEST", null);
						//comm.sendDatagram(InetAddress.getByName(potentialParent), REQUESTJOIN);
					}
					// Else if one of the children is consider closer to us, request child list
					else{
						System.out.println("Contacting node child with MinRTT for Child List. Child: " + potentialParent);
						sendCommand(NetworkAddressUtil.getHostIPString(potentialParent), "REQUEST-CHILDREN", null);
						//comm.sendDatagram(InetAddress.getByName(potentialParent), REQUESTCHILDLIST);
					}
				}
				else{
					System.out.println("Node returned no children. Requesting to join tree with parent set as: " + potentialParent);
					//comm.sendDatagram(InetAddress.getByName(potentialParent), REQUESTJOIN);
					sendCommand(NetworkAddressUtil.getHostIPString(potentialParent), "JOIN-REQUEST", null);
				}
				
			} catch (UnsupportedEncodingException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (UnknownHostException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				System.err.println("Cannot ping RTTS. ");				
				//e.printStackTrace();
			}
		}
		
	}
	
	private static void joinRequestReceived(String packetAddress) {
		System.out.println("Join Request Received from: " + packetAddress);
		if(!childNodeList.contains(packetAddress)){
			childNodeList.add(packetAddress);
		}
		membershipChanged(parentNode, childNodeList);
		// Reply with Ack so that client knows to update it's parent node value
		System.out.println("Join Request Approved, sending Ack to: " + packetAddress);
		//comm.sendDatagram(pkt.getAddress(), ACKJOIN);
		sendCommand(packetAddress, "JOIN-REPLY", null);
	}

	private static void joinRequestAcknowledged(String packetAddress){
		if(!parentNode.equals(packetAddress)){
			System.out.println("Join Ack Received from: " + packetAddress + ". Set as parent.");
			parentNode = packetAddress;
			membershipChanged(parentNode, childNodeList);
		}
		// Set off Timer to issue out refreshMessages
		deployTimer();
		// Set off Timer to seek path improvement
		//startImprovementTimer();
	}
	
	private static void childRequestReceived(String packetAddress){
		if(!packetAddress.equals(myAddress)){
			System.out.println("Child List Request Received from: " + packetAddress);
			String childlist = "";
			for (String child : childNodeList){
				childlist += " " + child;
			}
			// The format of the data in this packet will be "CHILDLIST x.x.x.x y.y.y.y z.z.z.z ..."		
			//comm.sendDatagram(pkt.getAddress(), CHILDLIST + childlist);
			sendCommand(packetAddress, "REPLY-CHILDREN", childlist);
		}
	}

	private static void leaveRequestReceived(String packetAddress){
				// If the leaving node was a child, simply remove node from childNodeList
		if (childNodeList.contains(packetAddress)){
			System.out.println("Leave Request Received from child at: " + packetAddress + ". Removing from child list.");
			
			childNodeList.remove(packetAddress);
			membershipChanged(parentNode, childNodeList);
		}
		// If node was your parent, looks like you've been abandoned, start looking :(
		else if (packetAddress == parentNode){
			// According to Fig. 4, you trace back to your grandparent from your rootPath
			// and start the join procedure again
			String grandparent;
			if(rootPath.length > 2)
				grandparent = rootPath[rootPath.length - 3];
			else
				grandparent = rootNode;
			potentialParent = grandparent;

			System.out.println("Leave Request Received from parent at: " + packetAddress + ". Restarting join algorithm.");
			//comm.sendDatagram(InetAddress.getByName(potentialParent), REQUESTCHILDLIST);
			sendCommand(NetworkAddressUtil.getHostIPString(potentialParent), "REQUEST-CHILDREN", null);
		}
	}
	// Refreshes are sent from child to parent periodically
	private static void refreshUpdateReceived(String packetAddress){
		if(childNodeList.contains(packetAddress)){
			String rootpath = "";
			for (String path : rootPath){
				rootpath += " " + path;
			}
			System.out.println("Refresh packet received from child at: " + packetAddress + ". Sending Notify path reply.");
			//comm.sendDatagram(pkt.getAddress(),NOTIFYPATH + rootpath);
			sendCommand(packetAddress, "PATH-REPLY", rootpath);
		}
	}
	
	// Path updates are sent from parent to child as a response to refreshes
	//public void pathUpdateReceived(DatagramPacket pkt){
	private static void pathUpdateReceived(String packetAddress, String packetPayload){
		// Only deal with this if we got it from our parent, ignore otherwise
		if(packetAddress.equals(parentNode)){			

			System.out.println("path packet received from parent at: " + packetAddress + ". Updating rootPath.");
			// Get data, ignore "PATH ", split values up, create new array and
			// finally add yourself to complete your root path
			//String data = new String(pkt.getData(),0,pkt.getLength(),"Cp1252");
			String data = packetPayload;
			data = data.substring(data.indexOf(" ") + 1);
			String[] newPath = data.split("[ ]+");
			String[] temp = new String[newPath.length + 1];
			for (int i = 0; i < newPath.length; i++){
				temp[i] = newPath[i];
			}
			temp[temp.length - 1] = myAddress;
			rootPath = temp;
			// Reset timerRefreshSent so that we don't think we haven't got any replies
			timerRefreshSent = 0;
		}
	}

	private static void datagramReceived(String packetAddress){
		System.out.println("Datagram packet received from: " + packetAddress);
	}
	
	public static void contactRP(String rendezvousAddress) {
		rootNode = RendezvousConn.getRootNode(myAddress, rendezvousAddress);
		System.out.println("Connected to Rendezvous Server: " + rendezvousAddress + ", root node address is: "  + rootNode);
	}
	
	private static void bootstrapClient(String rendezvousServer) {
		try{
			childNodeList = new Vector<String>();
			myAddress = NetworkAddressUtil.getLocalHostAddress();
			parentNode = "";
			isJoining = false;
			isLeaving = false;
			minRTT = 1029438923;
			
			System.out.println("Starting RTT Server.");
			rttServer = new SimpleRTTServer();
			
			System.out.println("Starting RTT Client.");
			rttClient = new SimpleRTTClient();
			
			System.out.println("Initilizing REFRESH timer.");
			refreshTimer = new Timer();
			pathImprovementTimer = new Timer();

			System.out.println("Initalize the MultiAudioChat");
			mac = new MultiAudioChat();
			
			contactRP(rendezvousServer);

			System.out.println("Starting Join.");
			startJoinTree();
		} catch(UnknownHostException e){
			System.err.println("Could not find host.");
			e.printStackTrace();
		} catch (IOException e) {
			System.err.println("Could not start RTT Server.");			
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) throws IOException {

		
		String rendezvousServer = null;
		if(args.length < 1) {
			rendezvousServer = "localhost";
			System.out.println("No Rendezvous Server defined, using localhost.");
		} else {
			rendezvousServer = args[0];
			System.out.println("Connecting to Rendezvous Server: " + rendezvousServer);
		}
		
		ServerSocket serverSocket = null;
		int portNumber = 4444;
		try {
			serverSocket = new ServerSocket(4445);
		} catch (IOException e) {
			System.err.println("Could not listen on port: " + portNumber);
			System.exit(1);
		}

		LauncherThread lt = new LauncherThread(serverSocket);
		lt.start();
		
		bootstrapClient(rendezvousServer);
		
	}
	
}